﻿// Copyright (c) 2017 GlassGrass

using System;
using System.Collections.Generic;
using System.Linq;
using System.Reflection;
using System.Text;
using UniRx;
using UnityEngine;

namespace GlassGrass.Unity.GuiFramework
	{
	/// <summary> GUI window. </summary>
	public class Window : IContainer, IBindable<string>
		{

		#region event

		/// <summary> Be fired when child was added. </summary>
		public IObservable<GuiElement> ChildAdded => _containerImpl.ChildAdded;
		
		/// <summary> Be fired when child was removed. </summary>
		public IObservable<GuiElement> ChildRemoved => _containerImpl.ChildRemoved;

		/// <summary> Be fired before this window is layouted. </summary>
		public IObservable<EventArgs> Layouting => _layouting;
		private readonly Subject<EventArgs> _layouting;

		/// <summary> Be fired after this window is layouted. </summary>
		public IObservable<EventArgs> Layouted => _layouted;
		private readonly Subject<EventArgs> _layouted;

		#endregion



		#region properties and fields

		#region framework system

		/// <summary> This window ID. </summary>
		public int WindowID { get; }

		/// <summary> [get] Mode of children layouting. </summary>
		public virtual LayoutMode ChildrenLayoutMode { get; }

		/// <summary> [get] Child GUI element of this </summary>
		public IList<GuiElement> Children => _containerImpl.Children;


		private readonly ContainerComponent _containerImpl = new ContainerComponent();

		#endregion



		#region layout parameters

		/// <summary> [get] Auto layouting mode option. Read only list. </summary>
		protected GUILayoutOption[] LayoutOptions
			{
			get
				{
				var list = new List<GUILayoutOption>( 8 )
					{
					GUILayout.ExpandWidth(ResizableWidth),
					GUILayout.ExpandHeight(ResizableHeight),
					};
				if( ResizableWidth )
					list.Add( GUILayout.Width( Size.x ) );
				if( ResizableHeight )
					list.Add( GUILayout.Height( Size.y ) );
				if( MinimumSize.HasValue )
					{
					list.Add( GUILayout.MinWidth( MinimumSize.Value.x ) );
					list.Add( GUILayout.MinHeight( MinimumSize.Value.y ) );
					}
				if( MaximumSize.HasValue )
					{
					list.Add( GUILayout.MaxWidth( MaximumSize.Value.x ) );
					list.Add( GUILayout.MaxHeight( MaximumSize.Value.y ) );
					}
				return list.ToArray();
				}
			}



		/// <summary> [get/set] If true then this element is visible. </summary>
		public bool Visible { get; set; } = true;

		/// <summary> [get/set] If true then users are allowed to drag this window. </summary>
		public bool Draggable { get; set; } = false;

		/// <summary> [get/set] If true then this window is clamped into screen region. </summary>
		public bool ClampedToScreen { get; set; } = true;

		/// <summary> [get/set] GUI element layouting relative location and size. </summary>
		public Rect Rect
			{
			get => _rect;
			set => _rect = value;
			}
		private Rect _rect;

		/// <summary> [get/set] GUI element layouting relative location. </summary>
		public virtual Vector2 Location
			{
			get => _rect.position;
			set => _rect.position = value;
			}

		/// <summary> [get/set] GUI element layouting size. </summary>
		public virtual Vector2 Size
			{
			get => _rect.size;
			set => _rect.size = value;
			}

		/// <summary> [get/set] GUI element layouting minimum size. </summary>
		public Vector2? MinimumSize { get; set; }

		/// <summary> [get/set] GUI element layouting maximum size. </summary>
		public Vector2? MaximumSize { get; set; }

		/// <summary> [get/set] Whether to allow element to resize automatically. </summary>
		public bool ResizableWidth { get; set; } = true;

		/// <summary> [get/set] Whether to allow element to resize height automatically. </summary>
		public bool ResizableHeight { get; set; } = true;

		/// <summary> [get] Window-to-screen clamping offset.  </summary>
		public RectOffset ClampingOffset { get; } = new RectOffset();

		/// <summary> [get/set] Rect where accepts dragging. </summary>
		public Rect DragRect { get; set; }

		#endregion



		#region contents

		/// <summary> [get] GUI content </summary>
		public GUIContent Content { get; } = new GUIContent();

		/// <summary> [get/set] Text of GUI content </summary>
		public virtual string Text
			{
			get => Content.text;
			set => Content.text = value;
			}

		/// <summary> [get/set] Image of GUI content </summary>
		public virtual Texture Image
			{
			get => Content.image;
			set => Content.image = value;
			}

		#endregion



		#region style parameters

		/// <summary><para> [get/set] GUI skin for itself and children </para>
		///          <para> NOTE: null-value will be inherited from parent skin.</para>
		/// </summary>
		public GUISkin CustomSkin { get; set; } = null;

		/// <summary><para> [get/set] GUI skin for itself and children </para>
		///          <para> NOTE: null-element will be inherited from parent skin.</para>
		/// </summary>
		public GUIStyle Style { get; set; } = null;

		#endregion

		#endregion



		/// <summary></summary>
		public Window(LayoutMode childrenLayoutMode)
			{
			WindowID = UnityEngine.Random.Range( 1000, 2000000 ) + Utility.AssemblyName.GetHashCode();
			ChildrenLayoutMode = childrenLayoutMode;
			_layouting = new Subject<EventArgs>();
			_layouted = new Subject<EventArgs>();
			}



		/// <summary> Call this method in MonoBehaviour.OnGUI to layout GUI. </summary>
		public void OnGui()
			{
			if (!Visible)
				return;

			if (ClampedToScreen)
				Rect = Rect.ClampToScreen(ClampingOffset);

			_layouting.OnNext(EventArgs.Empty);

			Rect = ChildrenLayoutMode == LayoutMode.Manual
				? GUI.Window( WindowID, Rect, RenderWindowInternal, Content, SelectValidStyle( SelectValidSkin( GUI.skin ) ) )
				: GUILayout.Window( WindowID, Rect, RenderWindowInternal, Content, SelectValidStyle( SelectValidSkin( GUI.skin ) ) );

			_layouted.OnNext(EventArgs.Empty);
			}



		/// <summary> Define what to choose style for this from skin. </summary>
		/// <param name="skin"></param>
		/// <returns></returns>
		protected GUIStyle ChooseStyle(GUISkin skin)
			=> skin?.window;



		/// <summary> Begin 'Text' property binding. </summary>
		/// <typeparam name="TValue"> Type of model property. </typeparam>
		/// <param name="modelObj"> Model object instance. </param>
		/// <param name="propertyName"> Model object property for binding. </param>
		/// <param name="converter"> Value converter. </param>
		/// <returns></returns>
		public Binding<TValue, string> BeginBinding<TValue>(object modelObj, string propertyName, IValueConverter<TValue,string> converter)
			=> GuiBindingFactory.BeginBinding( modelObj, propertyName, this, nameof( Text ), converter );



		/// <summary> Window layouting callback. </summary>
		/// <param name="id"></param>
		private void RenderWindowInternal(int id)
			{
			var act =
				(ChildrenLayoutMode == LayoutMode.Manual)
				? new Action<GuiElement>( child => child.LayoutManual( CustomSkin ?? GUI.skin ))
				: new Action<GuiElement>( child => child.LayoutAuto( CustomSkin ?? GUI.skin ));
			foreach( var child in Children.Where( c => c.Visible ) )
				act(child);

			if( Draggable )
				if( DragRect.width == 0 && DragRect.height == 0 )
					GUI.DragWindow();
				else
					GUI.DragWindow( DragRect );
			}



		/// <summary> Select valid skin. </summary>
		/// <param name="parentSkin"></param>
		/// <returns></returns>
		internal GUISkin SelectValidSkin(GUISkin parentSkin)
			=> CustomSkin ?? parentSkin ?? GUI.skin;



		/// <summary> Select valid stle. </summary>
		/// <param name="baseSkin"></param>
		/// <returns></returns>
		internal GUIStyle SelectValidStyle(GUISkin baseSkin)
			=> Style ?? ChooseStyle( baseSkin );
		}



	}
